Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fusdc status manager states #10406

Merged
merged 9 commits into from
Nov 12, 2024
Merged

feat: fusdc status manager states #10406

merged 9 commits into from
Nov 12, 2024

Conversation

0xpatrickdev
Copy link
Member

@0xpatrickdev 0xpatrickdev commented Nov 5, 2024

closes: #10389

Description

  • StatusManager with OBSERVED, ADVANCED states tracked in local MapStore
  • StatusManager state updates via Settler and Advancer
  • CctpTxEvidence type, typeGuard, and fixtures

Security Considerations

See FastUSDC threat model

Scaling Considerations

  • includes a seenTxs SetStore that will keep track of every tx observed by fusdc contract to assert uniqueness

Documentation Considerations

Includes state diagram in exos/README.md and the usual jsdoc

Testing Considerations

Includes unit tests of exos with a mocked LCA.

Upgrade Considerations

None, unreleased

Copy link

cloudflare-workers-and-pages bot commented Nov 5, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: f3d1e36
Status: ✅  Deploy successful!
Preview URL: https://1f60b94a.agoric-sdk.pages.dev
Branch Preview URL: https://pc-fusdc-status-manager.agoric-sdk.pages.dev

View logs

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-status-manager branch from af07f34 to a4ae691 Compare November 6, 2024 00:32
packages/fast-usdc/src/constants.js Outdated Show resolved Hide resolved
packages/fast-usdc/src/exos/README.md Outdated Show resolved Hide resolved
// TODO FAILURE PATH -> put money in recovery account or .transfer to receiver
// TODO should we have a TxStatus for this?
throw makeError(
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this recoverable? It looks like it.

Suggested change
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
`⚠️ No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,

per Logging docs:

🚨 indicates something that should never happen.

⚠️ indicates something that is not expected but was recovered from.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 was intentional - I think we should leave it until we recover from here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to capture this in the StatusManager with an ORPHANED state, and add some logic in the observe() method to initiate a SETTLE / settlement

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's come back to this when we're going all the error cases

...mutableEntries[unsettledIdx],
status: TxStatus.SETTLED,
};
txs.set(key, harden(mutableEntries));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever delete from txs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the approach to remove entries once they get to Settled

* @param {bigint} amount
*/
settle(address, amount) {
const key = `${address}-${amount}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exo guard doesn't guarantee that address includes no - characters. This sets off my confusion-attack spidey-sense.

Consider...

Suggested change
const key = `${address}-${amount}`;
const key = JSON.stringify([address, `${amount}`]);

Copy link
Member Author

@0xpatrickdev 0xpatrickdev Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see this fixup: 5226323

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative: put the number first

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or split from the right? Hm

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no reqs currently to decompose the key, so I think the current approach is sound. If anything, JSON.stringify might be overkill vs simple concatenation.

This is now factored to its own function and easy to change in the future.

Copy link
Member

@turadg turadg Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no reqs currently to decompose the key

Good point. It could even be a hash function. Let's leave it as is for debug-ability and we can button up before release

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-status-manager branch 5 times, most recently from 4cb1978 to 5226323 Compare November 6, 2024 22:19
@0xpatrickdev 0xpatrickdev marked this pull request as ready for review November 6, 2024 22:21
@0xpatrickdev 0xpatrickdev requested a review from a team as a code owner November 6, 2024 22:21
Copy link
Member

@turadg turadg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff! Most of my feedback I'd be content to see a TODO so that we can land this and keep iterating.

packages/fast-usdc/src/constants.js Outdated Show resolved Hide resolved
packages/fast-usdc/src/constants.js Outdated Show resolved Hide resolved
packages/fast-usdc/src/exos/README.md Outdated Show resolved Hide resolved
packages/fast-usdc/src/exos/advancer.js Outdated Show resolved Hide resolved
*/
handleEvent(event) {
// observe regardless of validation checks
// TODO - should we only observe after validation checks?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's not a transaction unless it's valid.

but validation checks should be performed by the Feed exo so it only forwards what is legitimate. The advancer needn't be concerned with query params.

There's no Feed yet so this could have a TODO but please make the plan clear. Perhaps add it to the issue about making the feed

packages/fast-usdc/src/exos/status-manager.js Outdated Show resolved Hide resolved
return 0;
}
const current = txs.get(key);
// XXX do we need defensive logic to ensure multiple entries don't have
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a fine invariant to assert. it's recoverable by ignoring duplicates. we don't put that burden on the feed because it would have to keep a record of all transactions. this storage can do the job

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'll save that for a later PR, but we can use txHash + chainId to assert uniqueness.

Thinking we'd want a new SetStore to keep track of these, since the txs MapStore ejects entries once they're Settled?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 6883b74

txs.set(key, harden(mutableEntries));
},
/**
* @overload
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL! you can also do this in a TS expression inside a @type comment. give that a try because it would be harder to miss. (My eyes only looked up to the lowest /** and didn't think to keep searching)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok iIll try that. This was a pain to wrangle and I wasn't happy with the current outcome

Copy link
Member Author

@0xpatrickdev 0xpatrickdev Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No luck with this:

/**
 * @type {((address: NobleAddress, amount: bigint) => StatusManagerValue[]) | ((address: NobleAddress, amount: bigint, index: number) => StatusManagerValue | undefined)}
 */

Was also playing around with these:

/**
 * @type {<T extends number | undefined = undefined>(
 *   address: NobleAddress, 
 *   amount: bigint, 
 *   index?: T
 * ) => T extends number ? StatusManagerValue | undefined : StatusManagerValue[]}
 */
/**
 * @template {number | undefined} [T=undefined]
 * @param {NobleAddress} address
 * @param {bigint} amount
 * @param {T} [index]
 * @returns {T extends number ? StatusManagerValue | undefined : StatusManagerValue[]}
 */

Copy link
Member Author

@0xpatrickdev 0xpatrickdev Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't know if the index logic in StatusManager is here to say. Some of the convos in other threads lead me to believe we won't need it. So not going to spin wheels too hard on this rn.

Edit: the index logic did not stay around, so this is irrelevant

packages/fast-usdc/src/typeGuards.js Show resolved Hide resolved
import { makeError, q } from '@endo/errors';

/**
* Very minimal 'URL query string'-like parser that handles:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we not have access to URL in XS?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure - but agoric123?foo=bar is not accepted in a URL constructor in a web console.

qs, query-string, and querystringify are well-known libraries we might consider using if this were to be provided by @agoric/orchestration. This particular contract seems to have much simpler reqs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't have URL in XS, I'm pretty sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see the tests.

qs-lite.min.js in https://github.com/kawanet/qs-lite is 530 bytes. Maybe easier to source that to get a more conventional qs ability

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we can get by without decodeURIComponent for now but consider mentioning in the code that it's available in XS

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-status-manager branch 6 times, most recently from 444aedd to 88f51f3 Compare November 8, 2024 15:14
@0xpatrickdev 0xpatrickdev requested a review from turadg November 8, 2024 15:15
*
* @enum {(typeof WriterTxStatus)[keyof typeof WriterTxStatus]}
*/
export const WriterTxStatus = /** @type {const} */ ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helpful distinction. please make the unqualified name be this all-encompassing enum. Observed and Advanced are the subset of a transaction status that are recorded in local state.

Settled --> [*]
```

### Complete state diagram (starting from OCW)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WFM

Comment on lines 109 to 114
/** @type {ChainAddress} */
const destination = harden({
chainId,
value: EUD,
encoding: /** @type {const} */ ('bech32'),
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth a helper fn

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, what would we call it? toChainAddress might not capture the bech32 we're adding. toBech32ChainAddress seemed too verbose where it wasn't worth factoring out.

Maybe ChainHub's getChainInfoByAddress should do this and instead be getChainAddress(addr)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to putting it on ChainHub since chainId is used here only for this construction.

We shouldn't reuse getChainInfoByAddress because this is making a value, not getting one.

consider,

Suggested change
/** @type {ChainAddress} */
const destination = harden({
chainId,
value: EUD,
encoding: /** @type {const} */ ('bech32'),
});
const destination = chainHub.makeChainAddress(EUD);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, please see 990a6df. I decided not to label this a breaking change since getChainInfoByAddress has only been around for ~a week, but let me know if I should amend the commit message.

observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
hasPendingSettlement: M.call(M.string(), M.bigint()).returns(M.boolean()),
settle: M.call(M.string(), M.bigint()).returns(M.undefined()),
view: M.call(M.string(), M.bigint()).returns(M.arrayOf(PendingTxShape)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a new name for what is usually called lookup. It also reads as if it could be a state transition. Please change.

Comment on lines 108 to 117
assertNotSeen(evidence);
const key = getPendingTxKey(evidence);
const entry = { ...evidence, status: TxStatus.Observed };

appendToStoredArray(
// @ts-expect-error index signature for type 'string' is missing in type 'PendingTx'.
pendingTxs,
key,
entry,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please DRY the duplication with advance using a helper in the prepare scope. e.g.

Suggested change
assertNotSeen(evidence);
const key = getPendingTxKey(evidence);
const entry = { ...evidence, status: TxStatus.Observed };
appendToStoredArray(
// @ts-expect-error index signature for type 'string' is missing in type 'PendingTx'.
pendingTxs,
key,
entry,
);
addTransaction(evidence, TxStatus.Observed);

import { makeError, q } from '@endo/errors';

/**
* Very minimal 'URL query string'-like parser that handles:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see the tests.

qs-lite.min.js in https://github.com/kawanet/qs-lite is 530 bytes. Maybe easier to source that to get a more conventional qs ability

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-status-manager branch 3 times, most recently from aa7d0dd to 6883b74 Compare November 11, 2024 16:41
* @param {CctpTxEvidence} evidence
*/
handleTransactionEvent(evidence) {
// XXX assume EventFeed performs validation checks. Advancer will assert uniqueness.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not tech debt, this is part of the design

config => harden(config),
{
/**
* XXX For now, assume this is invoked when a new entry is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why make this assumption? The advancer should not be concerned

requestedAmount,
);

// mark as Advanced since we're initiating the advance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already started

Suggested change
// mark as Advanced since we're initiating the advance
// mark as Advanced since `.transfer` initiates the advance

Comment on lines 32 to 33
/** composite of NobleAddress and transaction amount value */
export type PendingTxKey = `"["${NobleAddress}",${bigint}]"`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be parsable.

Suggested change
/** composite of NobleAddress and transaction amount value */
export type PendingTxKey = `"["${NobleAddress}",${bigint}]"`;
/** composite of NobleAddress and transaction amount value */
export type PendingTxKey = `"pendingTx:${string}`;

* @param {bigint} amount
*/
const toPendingTxKey = (addr, amount) =>
/** @type {PendingTxKey} */ (JSON.stringify([addr, String(amount)]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please make clear in the docs that this isn't meant to be parseable. see also PendingTxKey type comment

import { makeError, q } from '@endo/errors';

/**
* Very minimal 'URL query string'-like parser that handles:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we can get by without decodeURIComponent for now but consider mentioning in the code that it's available in XS

* @returns {boolean}
*/
hasQueryParams: address => {
return address.includes('?');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agoric1foo?bar?baz would return true but is invalid. for this to be true it getQueryParams should not error.

Suggested change
return address.includes('?');
try {
addressTools.getQueryParams(address);
return result.params.length > 0;
} catch {
return false;
}

Comment on lines 7 to 11
const txStatuses = values(TxStatus);
const invalidStatuses = values(PendingTxStatus).find(
status => !txStatuses.includes(status),
);
t.is(invalidStatuses, undefined, `${invalidStatuses} not in TxStatus`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thrown off by the comparison with undefined.

Suggested change
const txStatuses = values(TxStatus);
const invalidStatuses = values(PendingTxStatus).find(
status => !txStatuses.includes(status),
);
t.is(invalidStatuses, undefined, `${invalidStatuses} not in TxStatus`);
const allStatusus = new Set(values(TxStatus));
const pendingStatuses = new Set(values(PendingTxStatus));
const pendingMinusAll = pendingStatuses.difference(allStatuses);
t.is(pendingMinusAll.size, 0);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main intention here was to ensure the extraneous value name(s) are in the error message. TIL about Set.difference, pretty cool. Unfortunately in our testing env, I'm seeing pendingStatuses.difference is not a function. I went with a slightly revised approach that's hopefully more readable: 9c14032

Copy link
Member

@turadg turadg Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's not available until Node 22. When we can use it, isSubsetOf would be even better. But the filter is fine.

packages/fast-usdc/test/utils/address.test.ts Outdated Show resolved Hide resolved
packages/orchestration/src/exos/chain-hub.js Show resolved Hide resolved
@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-status-manager branch 2 times, most recently from 8acd20c to a92b0ec Compare November 11, 2024 19:54
},

/**
* Lookup all entries for a given address and amount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the pending entries. maybe also lookupPending in the name


export type LogFn = (...args: unknown[]) => void;

export interface PendingTx extends CctpTxEvidence {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should do more of this, interfaces extending interfaces

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-status-manager branch from a92b0ec to 9eac623 Compare November 12, 2024 19:21
@0xpatrickdev 0xpatrickdev added automerge:rebase Automatically rebase updates, then merge and removed automerge:rebase Automatically rebase updates, then merge labels Nov 12, 2024
- shows the various `StatusManager` MapStore states updates (`OBSERVED`, `ADVANCED`) via `Advancer` and `Settler` stubs
- use a composite key of `txHash+chainId` to track unique `EventFeed` submissions
@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-status-manager branch from 9eac623 to f3d1e36 Compare November 12, 2024 19:27
@0xpatrickdev 0xpatrickdev added the automerge:rebase Automatically rebase updates, then merge label Nov 12, 2024
@mergify mergify bot merged commit c236c55 into master Nov 12, 2024
91 checks passed
@mergify mergify bot deleted the pc/fusdc-status-manager branch November 12, 2024 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge:rebase Automatically rebase updates, then merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Status Manager usable by contract
3 participants